package IC.SEMANTIC;

import IC.AST.*;
import IC.Symbols.Kind;
import IC.Symbols.Symbol;
import IC.Types.TypeTable;

import java.util.*;

public class InitializationTestVisitor implements Visitor {

    private int symbolLevel;
    private Object semantic;
    private Set<Symbol> symbolsSet;
    private TypeTable typeTable;
    private List<SemanticWarning> semanticWarningsList = new ArrayList<SemanticWarning>();

    public InitializationTestVisitor(TypeTable typeTable) {
        this.typeTable = typeTable;
    }

    private void addNewSymbol(Symbol newSym) {
        if (symbolLevel <= newSym.getLevel()) {
            newSym.setSemantic(semantic);
            newSym.setSymbolLevel(symbolLevel);
            symbolsSet.add(newSym);
        }
    }
	
	private void initSymbolProperties(Symbol symbol) {
		symbol.setSemantic(null);
		symbol.setSymbolLevel(Integer.MAX_VALUE);
	}
    
    public Object visit(Program icProgram) {
        for (ICClass icClass : icProgram.getClasses())
            icClass.accept(this);
        return null;
    }
    
    public Object visit(ICClass icClass) {
        for (Method method : icClass.getMethods())
            method.accept(this);
        return null;
    }
    
    public Object GenericMethodVisit(Method method) {
        symbolsSet = new HashSet<Symbol>();
        semantic = method;
        symbolLevel = 0;

        for (Statement statement : method.getStatements())
            statement.accept(this);

        if (method.getSemanticType().getMethodReturnType() != typeTable.getVoidPrimitiveType() && method.getReturnSymbolTableSymbol().getLevel()!=0) {
        	String warningMessage = String.format("there are paths that Method '%s' may not return values on.", method.getName());
            semanticWarningsList.add(new SemanticWarning(method.getLine(), warningMessage));
        }
        return null;
    }

    
    public Object visit(VirtualMethod icVirtualMethod) {
        return GenericMethodVisit(icVirtualMethod);
    }

    public Object visit(StaticMethod icStaticMethod) {
        return GenericMethodVisit(icStaticMethod);
    }

    public Object visit(Assignment icAssignment) {
        
    	Location variableLocation = icAssignment.getVariable();
        icAssignment.getAssignment().accept(this);

        if (variableLocation instanceof VariableLocation) {
            VariableLocation castedVariableLocation = (VariableLocation)variableLocation;
            if (!castedVariableLocation.isExternal()) 
                addNewSymbol(castedVariableLocation.getSymbolTableEntry());
        } else 
        	variableLocation.accept(this);
        return null;
    }
    
    public Object visit(CallStatement icCallStatement) {
        return icCallStatement.getCall().accept(this);
    }
    
    public Object visit(Return icReturn) {
        addNewSymbol(icReturn.getSymbolTableEntry());
        return null;
    }
    
    public Object visit(If icIf) {
        icIf.getCondition().accept(this);

        Object originalSemantic = semantic;
        Set<Symbol> originalSymbols = symbolsSet;

        semantic = icIf.getOperation();
        symbolsSet = new HashSet<Symbol>();
        symbolLevel++;

        icIf.getOperation().accept(this);
        Set<Symbol> symbolsPathes = null;

        if (icIf.hasElse()) {
            Set<Symbol> origSymbolsSet = symbolsSet;
			for (Symbol symbol : symbolsSet)
				initSymbolProperties(symbol);
			
            semantic = icIf.getElseOperation();
            icIf.getElseOperation().accept(this);
            
            symbolsSet = new HashSet<Symbol>();
            Set<Symbol> initOnElseOp = symbolsSet;

            //initialized on both
            origSymbolsSet.retainAll(initOnElseOp);
            symbolsPathes = origSymbolsSet;
        }
		
		for (Symbol symbol : symbolsSet)
			initSymbolProperties(symbol);

        symbolLevel--;
        semantic = originalSemantic;
        symbolsSet = originalSymbols;

        if (symbolsPathes != null)
            for (Symbol symbol : symbolsPathes)
                addNewSymbol(symbol);

        return null;
    }
  
    public Object visit(While icWhile) {
        icWhile.getCondition().accept(this);

        Object origSemantic = semantic;
        Set<Symbol> origSymbolsSet = symbolsSet;

        semantic = icWhile.getOperation();
        symbolsSet = new HashSet<Symbol>();
        symbolLevel++;

        icWhile.getOperation().accept(this);
		
		for (Symbol symbol : symbolsSet)
			initSymbolProperties(symbol);

        symbolLevel--;
        semantic = origSemantic;
        symbolsSet = origSymbolsSet;

        return null;
    }
    
    public Object visit(StatementsBlock statementsBlock) {
        for (Statement statement : statementsBlock.getStatements())
            statement.accept(this);
        return null;
    }

    public Object visit(LocalVariable icLocalVariable) {
        if (icLocalVariable.hasInitValue()) {
            icLocalVariable.getInitValue().accept(this);
            addNewSymbol(icLocalVariable.getSymbolTableEntry());
        }
        return null;
    }
    
    public Object visit(VariableLocation icVariableLocation) {	
        Symbol variableSymbol = icVariableLocation.getSymbolTableEntry();
        if (variableSymbol.getKind() == Kind.Variable && (symbolLevel < variableSymbol.getLevel() || (symbolLevel ==variableSymbol.getLevel() && semantic!=variableSymbol.getSemantic()))) {
        	String warningMessage = String.format("possibly not initialized Variable '%s'.", icVariableLocation.getName());
        	this.semanticWarningsList.add(new SemanticWarning(icVariableLocation.getLine(), warningMessage));
        }
        return null;
    }
    
    public Object visit(ArrayLocation icArrayLocation) {
        icArrayLocation.getArray().accept(this);
        icArrayLocation.getIndex().accept(this);
        return null;
    }
    
    public Object visit(StaticCall icStaticCall) {
        for (Expression argumentExpression : icStaticCall.getArguments())
            argumentExpression.accept(this);
        return null;
    }
    
    public Object visit(VirtualCall icVirtualCall) {
        for (Expression argumentExpression : icVirtualCall.getArguments())
        	argumentExpression.accept(this);
        if (icVirtualCall.isExternal())
            icVirtualCall.getLocation().accept(this);
        return null;
    }
    
    public Object visit(NewArray icNewArray) {
        return icNewArray.getSize().accept(this);
    }
    
    public Object visit(Length icLength) {
        return icLength.getArray().accept(this);
    }

    public Object visit(MathBinaryOp icMathBinaryOp) {
        icMathBinaryOp.getFirstOperand().accept(this);
        icMathBinaryOp.getSecondOperand().accept(this);
        return null;
    }
    
    public Object visit(LogicalBinaryOp icLogicalBinaryOp) {
        icLogicalBinaryOp.getFirstOperand().accept(this);
        icLogicalBinaryOp.getSecondOperand().accept(this);
        return null;
    }
    
    public Object visit(MathUnaryOp icMathUnaryOp) {
        return icMathUnaryOp.getOperand().accept(this);
    }
    
    public Object visit(LogicalUnaryOp icLogicalUnaryOp) {
        return icLogicalUnaryOp.getOperand().accept(this);
    }
    
    public Object visit(Literal icLiteral) {
    	// nothing to do here
        return null;
    }

    public Object visit(LibraryMethod icLibraryMethod) {
    	// nothing to do here
        return null;
    }
    
    public Object visit(Formal icFormal) {
    	// nothing to do here
        return null;
    }

    public Object visit(PrimitiveType icPrimitiveType) {
    	// nothing to do here
        return null;
    }
    
    public Object visit(UserType icUserType) {
    	// nothing to do here
        return null;
    }
    
    public Object visit(This icThis) {
    	// nothing to do here
        return null;
    }
    
    public Object visit(NewClass newClass) {
    	// nothing to do here
        return null;
    }
    
    public Object visit(Break icBreak) {
        // nothing to do here
        return null;
    }
    
    public Object visit(Continue icContinue) {
    	// nothing to do here
        return null;
    }
    
    public Object visit(Field field) {
        return null;
    }

    public Object visit(ExpressionBlock icExpressionBlock) {
        return icExpressionBlock.accept(this);
    }

    public List<SemanticWarning> getSemanticWarningsList() {
        return semanticWarningsList;
    }
}
